Skip to content

feat(types): refuse wrong-dialect functions and missing sources at the type level - #106

Merged
Makisuo merged 10 commits into
MapleTechLabs:mainfrom
artile:feat/type-level-dialect
Nov 2, 2026
Merged

Makisuo merged 10 commits into
MapleTechLabs:mainfrom
artile:feat/type-level-dialect

Conversation

@artile

@artile artile commented Oct 25, 2026 •

Copy link
Copy Markdown
Contributor

Db.run and compile find a function of the wrong dialect only when the query compiles, as a defect. A column of a source that is not in the query's FROM or JOIN is found only by the server. Both are now type errors, with a message that names the cause.

Closes #66.
Part of #52.

Depends on #97; review only the commits after it.

Changes

  • New DialectEntry<Set>. It rides in the params channel that a query already carries. Each built-in function adds ClickHouseOnly or PostgresOnly to its result. A portable function (nullIf, /clickhouse's lower) and the shared operators add nothing. The tag follows the same set that assertSet checks at run time.
  • Dialect<Functions>: clickhouseDialect is Dialect<"clickhouse">, postgresDialect is Dialect<"postgres">. A plain Dialect is not checked.
  • compile, compileUnsafe, compileUnion and compileUnionUnsafe check DialectSatisfied<Params, Functions>. The target is options.dialect, else the entry's dialect. New types Compile<Default>, CompileUnsafe<Default>, CompileUnion<Default>, CompileUnionUnsafe<Default> and the options types QueryCompileOptions and UnionCompileOptions, so /postgres gets a real "postgres" default (it used typeof compileCH before). The query overload is now the last one, so a refused query shows its own reason (wrongDialect, paramsRequired) and not the write overload's.
  • fromSqlClient returns DatabaseApi<Functions>. With postgresDialect, run refuses ClickHouse functions in the type. DatabaseApi<"postgres"> still provides the Database tag.
  • A query with functions of two dialects is refused everywhere (mixedDialects), the Database in context and custom dialects included.
  • New SourceEntry<Name>. A column ref carries its source: the FROM alias, else the table name, or the join alias. CHQuery gets a sixth type parameter, Source (default never). Each query method removes its own sources (BindSources). An entry that is left names a missing source, and compile and run refuse it (sourceNotInQuery). A later JOIN of that name also provides it.
  • Correlation: an inner query can use the outer query's accessor (e.Name.eq($.Name), which writes s.Name). The inner query carries s on, and the outer query provides it. Docs show this as a checked alternative to a string outerRef.
  • Fix: orderBy(($) => [...]) carried only the first sort key's params. A param in a later key was not required. It now carries all of them.
  • The runtime QueryBuilderDefect for a wrong-dialect function stays as the safety net. soundness.test.ts keeps testing it under @ts-expect-error.
  • Tests: src/ch/dialect.test-d.ts and src/ch/sources.test-d.ts (@ts-expect-error for each refused case, expectTypeOf for each message), an orderBy case in params-propagation.test-d.ts, and a runtime docs test for the correlated accessor.
  • Docs: new section "Checked in the type" in docs/params-and-compilation.md, plus docs/reference.md, docs/postgres.md, docs/database.md, docs/joins-and-subqueries.md and design/dialects.md. One minor changeset.

Left out

  • A CompiledQuery does not carry its dialect in the type. pg.run(CH.compileUnsafe(q)) is still refused only at run time (DialectMismatch). Adding it changes CompiledQuery for every caller.
  • The Database tag stays untyped (DatabaseApi<string>), so Db.run checks mixing only. A typed tag per dialect is an API decision for the maintainers.
  • CurrentDialect is read at run time, so the type cannot see it. Under a CurrentDialect of another dialect, pass options.dialect too.
  • No runtime check for a missing source. The server still reports it, and a captured ref to a table of the same name in a subquery still binds to the inner table, as SQL does.
  • Writes (update, deleteFrom, insertInto) do not tag their own accessors. A ref from a select query used in a write is still refused.
  • A source whose name the type does not know (Table<string, …>, a non-literal alias) is not checked.
  • Effect on existing code: a ClickHouse function's result type is now Expr<T, ClickHouseOnly>. Expr<T> annotations still accept it, but toEqualTypeOf<Expr<T>> assertions change. Thirteen such assertions in three test files of this repo are updated.
  • No literal, non-empty check for derived-source aliases (effect-qb's third point). FreshAlias already covers clashes.
  • MySQL, SQLite and D1 are not part of this PR. A new dialect only needs Dialect<"its-set"> and builtins("its-set", …).

Validation

  • bun run build: pass.
  • bun run typecheck: pass.
  • bunx vitest run --testTimeout=60000 --hookTimeout=60000 with ClickHouse 26.8.2.7 at 127.0.0.1:8123 (EFFECT_ORM_CLICKHOUSE_* set): 60 files pass, 0 skipped; 1040 tests pass.
  • node scripts/check-doc-citations.mjs: ok (25 files). node scripts/check-exports-documented.mjs: ok (303 /clickhouse, 221 /postgres). node scripts/check-doc-examples.mjs: ok (21 files).
  • bun scripts/check-package.ts: pass (tarball imports, types, CommonJS consumer).
  • Upstream CI for a fork PR waits for a maintainer to approve the run.

A built-in function adds a DialectEntry for its set to its result's
params. The entry travels with the params into the query, and compile,
compileUnion and DatabaseApi<Functions>.run check it against the target
dialect. Functions of two sets in one query are refused everywhere.

Also fix orderBy(callback): it kept only the first sort key's params.
A column ref carries a SourceEntry for its source: the FROM alias or table
name, or the join alias. Each query removes the entries of its own sources,
so an entry left on a query names a source it is missing. compile,
compileUnion and run refuse it in the type. A correlated subquery's outer
column is provided by the outer query that embeds it.
@coderabbitai

coderabbitai Bot commented Oct 25, 2026 •

Copy link
Copy Markdown

Warning

Review limit reached

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Next included review available in 29 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

Learn how review limits work.

Review configuration:

Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 5a81abd1-ac5e-4529-b4d7-9ba732522c64
Commits

Reviewing files that changed from the base of the PR and between b57faf1 and c3dfdbe.

Files selected for processing: 48
  • .changeset/dialect-in-context.md
  • .changeset/type-level-dialect.md
  • design/dialects.md
  • docs/database.md
  • docs/joins-and-subqueries.md
  • docs/params-and-compilation.md
  • docs/postgres.md
  • docs/reference.md
  • docs/sqlite.md
  • src/ch/any-boundaries.test-d.ts
  • src/ch/compile.ts
  • src/ch/deep-codecs.test.ts
  • src/ch/dialect.test-d.ts
  • src/ch/dialect.test.ts
  • src/ch/dialect.ts
  • src/ch/expr.test-d.ts
  • src/ch/expr.ts
  • src/ch/failure.ts
  • src/ch/functions/aggregate.ts
  • src/ch/functions/array.ts
  • src/ch/functions/builtin.ts
  • src/ch/functions/conditional.ts
  • src/ch/functions/date-time.ts
  • src/ch/functions/map.ts
  • src/ch/functions/numeric.ts
  • src/ch/functions/string.ts
  • src/ch/functions/window.ts
  • src/ch/params-propagation.test-d.ts
  • src/ch/predicates.test.ts
  • src/ch/query.ts
  • src/ch/soundness.test.ts
  • src/ch/sources.test-d.ts
  • src/ch/subquery-context.ts
  • src/core.ts
  • src/database/database.ts
  • src/docs-examples.test.ts
  • src/pg/dialect.ts
  • src/pg/functions.ts
  • src/postgres.ts
  • src/sql/fiber-local.ts
  • src/sql/literal-binder.ts
  • src/sql/render-tracker.ts
  • src/sql/sql-syntax.ts
  • src/sqlite.ts
  • src/sqlite/dialect.ts
  • src/sqlite/functions.ts
  • src/sqlite/sqlite.test.ts
  • tests/fuzz/registry.fuzz.test.ts
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

Share

Makisuo and others added 4 commits November 2, 2026 16:28
The /sqlite entry (added on main after this branch) still chose its default
dialect when the call was made and ignored CurrentDialect. It now resolves
options.dialect, then CurrentDialect, then sqliteDialect when the Effect runs,
like /postgres. D1 can be provided once as CurrentDialect.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…feat/type-level-dialect

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

# Conflicts:
#	src/ch/dialect.ts
…es in the type

main's SQLite and D1 dialects did not exist when the dialect tagging was
written. SQLite functions now carry SqliteOnly, sqliteDialect and d1Dialect
are Dialect<"sqlite">, and the /sqlite compile functions check against
"sqlite", so a SQLite query with a ClickHouse or Postgres function (and the
reverse) is a type error, as it already was a compile-time defect.

Row locks (MapleTechLabs#85) ride the same channel as a ClauseEntry: forUpdate and the
other locks add RowLock<Clause>, which DialectSatisfied refuses for any
target but Postgres (unsupportedClause), matching the compile's run-time
refusal. The mixedDialects message names three sets when there are three.

The fuzz registry guard looked for `: Dialect = {`; it now also matches
`: Dialect<"…"> = {`, or it found no dialect at all.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Makisuo

Makisuo commented Nov 2, 2026

Copy link
Copy Markdown
Contributor

Merge-readiness pass (maintainer side):

  • Merged the updated feat(compile): install the dialect through Effect context, not module state #97 branch (feat/dialect-context), which already has current main in it.
    • One conflict, in src/ch/dialect.ts. Kept functions?: Functions and main's new unionNumberOrderKeys.
  • Extended the type-level dialect tagging to main's SQLite/D1 dialects:
    • SqliteOnly = DialectEntry<"sqlite"> is on every /sqlite function result (count, sum, lower, coalesce, …), matching builtins("sqlite", …)'s run-time refusal.
    • sqliteDialect and d1Dialect are Dialect<"sqlite"> (one function set). The /sqlite compile functions are Compile<"sqlite"> / CompileUnsafe<"sqlite"> / CompileUnion<"sqlite"> / CompileUnionUnsafe<"sqlite">.
    • fromSqlClient(client, { dialect: d1Dialect }) is a DatabaseApi<"sqlite">.
    • The error labels know SQLite. mixedDialects names three sets when there are three.
  • Row locks on SQLite / D1: refuse in the type, document the replacement #85 (row locks refused in the type on SQLite/D1). This fell out naturally, so it is done here:
    • New ClauseEntry<Clause, Sets> on the same params channel. forUpdate / forNoKeyUpdate / forShare / forKeyShare add RowLock<Clause> (ClauseEntry<…, "postgres">).
    • DialectSatisfied refuses it for a ClickHouse, SQLite or D1 target (unsupportedClause: "FOR UPDATE has no meaning for the sqlite dialect"). This matches the compile's clauses.locking refusal. A custom plain Dialect stays unchecked.
    • The two runtime tests that compile a lock on ClickHouse/SQLite now carry @ts-expect-error, as soundness.test.ts does.
    • The replacement pattern (conditional update … returning, single-writer DO) is in the docs. format and distinctOn could use the same mechanism. I left that as a note in design/dialects.md.
  • Fixed a main-side guard this PR broke. tests/fuzz/registry.fuzz.test.ts found dialects by matching : Dialect = {, so with Dialect<"…"> annotations it found none. It now matches both forms.
  • Tests: SQLite/D1 and row-lock cases in src/ch/dialect.test-d.ts. I checked that each new @ts-expect-error fires for the intended wrongDialect / unsupportedClause reason. Docs and changeset updated. New exports SqliteOnly, ClauseEntry and RowLock are in the catalog.

Verified locally against ClickHouse 26.8:

  • bun run build and bun run typecheck pass.
  • bun run test: 73 files / 1437 tests, plus the doc and export checks.
  • Live tests/*.clickhouse.test.ts plus core.sqlite / core.postgres: 11 files / 285 tests.
  • vitest run tests/fuzz with FUZZ_SCALE=0.3: 269 tests.
  • bun scripts/check-package.ts passes.

Merge order: this conflicts with #102 (query.ts) and #103 (expr.ts and query.ts). #103 also adds a third ColumnRef type parameter called Source with a different meaning (the join alias, for not-NULL facts). Whichever lands second has to reconcile the two.

@Makisuo
Makisuo merged commit 80caf7c into MapleTechLabs:main Nov 2, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Dialect mismatch and missing sources as type errors

2 participants